package org.luxor.commons.mvc.filter;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.luxor.commons.core.utils.HttpContextUtils;
import org.luxor.commons.core.utils.Sign;
import org.luxor.commons.core.utils.StrKit;
import org.luxor.commons.core.utils.encrypt.AesEncryptUtils;
import org.luxor.commons.core.web.RestStatus;
import org.luxor.commons.core.web.data.R;
import org.luxor.commons.log.component.HttpResponseWrapper;
import org.luxor.commons.mvc.config.properties.ClientCredential;
import org.luxor.commons.mvc.config.properties.SignatureProperties;
import org.luxor.commons.mvc.wrapper.HttpRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;

/**
 * API接口签名拦截器
 *
 * @author Mr.yan  @date 2022/11/6
 */
public class SignatureFilter extends OncePerRequestFilter {
    private static final Logger log = LoggerFactory.getLogger(SignatureFilter.class);

    private static final PathMatcher pathMatcher = new AntPathMatcher();

    private static final List<MediaType> MEDIA_TYPES = Arrays.asList(MediaType.APPLICATION_JSON, MediaType.APPLICATION_JSON_UTF8,
            MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_XML,
            MediaType.ALL, MediaType.TEXT_PLAIN, MediaType.TEXT_XML);

    private static final int CAPACITY_SIZE = 5000;

    private static final Cache<String, String> NONCE_CACHE = CacheBuilder.newBuilder()
            .initialCapacity(CAPACITY_SIZE).expireAfterAccess(Sign.TIMESTAMP_TIMEOUT_MS, TimeUnit.MILLISECONDS)
            .build();

    private final SignatureProperties signatureProperties;

    public SignatureFilter(SignatureProperties signatureProperties) {
        this.signatureProperties = signatureProperties;
    }

    @Override
    public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (!signatureProperties.isEnabled()) {
            filterChain.doFilter(request, response);
            return;
        }

        if (!StringUtils.hasText(request.getContentType()) || !MEDIA_TYPES.contains(MediaType.parseMediaType(request.getContentType()))) {
            filterChain.doFilter(request, response);
            return;
        }

        for (String uriPattern : signatureProperties.excludeUrlPatterns()) {
            if (pathMatcher.match(uriPattern, request.getRequestURI())) {
                filterChain.doFilter(request, response);
                return;
            }
        }


        String clientId = request.getParameter(Sign.CLIENT_ID);
        if (!StringUtils.hasText(clientId)) {
            log.error("API接口签名错误，客户端ID(clientId)请求参数为空");
            HttpContextUtils.sendJson(response, R.failed(RestStatus.SIGN_ERROR.code(), "API接口签名错误，客户端ID(clientId)请求参数为空"));
            return;
        }
        ClientCredential clientCredential = null;
        List<ClientCredential> credentials = signatureProperties.getCredentials();
        for (ClientCredential credential : credentials) {
            if (clientId.equals(credential.getClientId())) {
                clientCredential = credential;
            }
        }
        if (clientCredential == null) {
            log.error("API接口签名错误，未找到对应客户端证书(ClientCredential)配置. clientId:{}", clientId);
            HttpContextUtils.sendJson(response, R.failed(RestStatus.SIGN_ERROR.code(), "API接口签名错误，未找到对应客户端证书(ClientCredential)配置"));
            return;
        }

        if (!clientCredential.getEnabledSignature() && !clientCredential.getEnabledEncryption()) {
            filterChain.doFilter(request, response);
            return;
        }

        // 签名验证
        if (clientCredential.getEnabledSignature()) {
            if (StringUtils.hasText(request.getContentType()) && MEDIA_TYPES.contains(MediaType.parseMediaType(request.getContentType()))) {
                HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
                boolean checkResult = checkSignature(clientCredential, requestWrapper, response, filterChain);
                if (!checkResult) {
                    return;
                }
            }
        }

        // 参数加解密
        if (clientCredential.getEnabledEncryption()) {
            HttpRequestWrapper requestWrapper = new HttpRequestWrapper(request);
            HttpResponseWrapper responseWrapper = new HttpResponseWrapper(response);
            encryptionHandler(clientCredential, requestWrapper, responseWrapper, filterChain);
            return;
        }
        filterChain.doFilter(request, response);
    }

    private void encryptionHandler(ClientCredential clientCredential, HttpRequestWrapper request, HttpResponseWrapper response, FilterChain filterChain) throws ServletException, IOException {
        // 解密
        String ciphertext = request.getContext() == null ? StrKit.EMPTY_JSON : new String(request.getContext(), StandardCharsets.UTF_8);
        String decryptContext = AesEncryptUtils.decrypt(ciphertext, clientCredential.getClientSecret());
        if (decryptContext == null) {
            log.error("加密报文解析失败");
            HttpContextUtils.sendJson(response, R.failed(RestStatus.ENCRYPT_ERROR.code(), "加密报文解析失败"));
            return;
        }
        request.setContext(decryptContext.getBytes());

        filterChain.doFilter(request, response);

        //加密
        String plaintext = response.getContext() == null ? StrKit.EMPTY : new String(response.getContext(), StandardCharsets.UTF_8);
        String cipherContext = AesEncryptUtils.encrypt(plaintext, clientCredential.getClientSecret());
        if (cipherContext == null) {
            log.error("响应报文加密失败");
            HttpContextUtils.sendJson(response, R.failed(RestStatus.ENCRYPT_ERROR.code(), "响应报文加密失败"));
            return;
        }
        response.setContext(cipherContext.getBytes(StandardCharsets.UTF_8), true);
    }

    private boolean checkSignature(ClientCredential clientCredential, HttpRequestWrapper request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 随机数(nonce)校验
        String nonce = request.getParameter(Sign.NONCE);
        if (!StringUtils.hasText(nonce)) {
            log.error("API接口签名错误，随机数(nonce)请求参数为空");
            HttpContextUtils.sendJson(response, R.failed(RestStatus.SIGN_ERROR.code(), "API接口签名错误，随机数(nonce)请求参数为空"));
            return false;
        }
        String cacheNonce = NONCE_CACHE.getIfPresent(nonce);
        if (cacheNonce == null) {
            NONCE_CACHE.put(nonce, nonce);
        } else {
            log.error("API接口签名错误，随机数(nonce)请求参数错误");
            HttpContextUtils.sendJson(response, R.failed(RestStatus.SIGN_ERROR.code(), "API接口签名错误，禁止重放攻击"));
            return false;
        }

        // 时间(timestamp)校验
        String timestampStr = request.getParameter(Sign.TIMESTAMP);
        if (!StringUtils.hasText(timestampStr)) {
            log.error("API接口签名错误，时间(timestamp)请求参数为空");
            HttpContextUtils.sendJson(response, R.failed(RestStatus.SIGN_ERROR.code(), "API接口签名错误，时间(timestamp)请求参数为空"));
            return false;
        }
        if (System.currentTimeMillis() - Long.parseLong(timestampStr) > Sign.TIMESTAMP_TIMEOUT_MS) {
            log.error("API接口签名错误，签名时间已过期(5分钟)");
            HttpContextUtils.sendJson(response, R.failed(RestStatus.SIGN_ERROR.code(), "API接口签名错误, 签名时间已过期"));
            return false;
        }

        String clientId = clientCredential.getClientId();

        String clientSecret = clientCredential.getClientSecret();
        if (!StringUtils.hasText(clientSecret)) {
            log.error("API接口签名错误，客户端秘钥(clientSecret)配置参数为空. clientId:{}", clientId);
            HttpContextUtils.sendJson(response, R.failed(RestStatus.SIGN_ERROR.code(), "API接口签名错误，客户端秘钥(clientSecret)配置参数为空"));
            return false;
        }

        TreeMap<String,String> requestParams = new TreeMap<>();
        Enumeration<String> paramIts = request.getParameterNames();
        while (paramIts.hasMoreElements()) {
            String name = paramIts.nextElement();
            String value = request.getParameter(name);
            requestParams.put(name, value);
        }
        byte[] requestBody = request.getContext();

        // 生成签名串
        String signPlainText = Sign.makeSignPlainText(clientSecret, requestParams, requestBody);
        String sign = Sign.sign(clientSecret, signPlainText, Sign.SIG_METHOD);

        log.debug("API接口签名验证，clientId:{} clientSecret:{} sign:{}", clientId, clientSecret, sign);

        // 数字签名(signature)校验
        if (!sign.equals(request.getParameter(Sign.SIGNATURE))) {
            log.error("API接口签名错误，clientId:{} clientSecret:{} signPlainText:{}", clientId, clientSecret, signPlainText);
            HttpContextUtils.sendJson(response, R.failed(RestStatus.SIGN_ERROR));
            return false;
        }
        return true;
    }
}
